//@version=5
indicator("30 Min Pivot Enhanced", overlay=true)

// === Inputs ===
trendLength         = input.int(4, title="Consecutive (Same-Color) Trend Candles Before Pivot", minval=1)
maxBarsAfterPivot   = input.int(2, title="Maximum Candles Allowed After Pivot for Confirmation", minval=1)
showPivotLines      = input.bool(false, title="Show Pivot Line")
enableBuyAlerts     = input.bool(true,  title="Enable Buy Alerts")
enableSellAlerts    = input.bool(false, title="Enable Sell Alerts")

// --- Flush detection tuning (simplified)
useFlushDetect      = input.bool(true,  title="Use flush detection (big red body)")
atrLen              = input.int(14,    title="ATR Length (for flush)", minval=1)
flushBodyMult       = input.float(1.5, title="Flush body size threshold (ATR multiplier)", step=0.1)
// rangeMA_len         = input.int(20,    title="Body MA lookback (bars)", minval=2)
// bodyMultMA          = input.float(1.15,title="Flush threshold vs avg body (multiplier)", step=0.05)

flushLookback       = input.int(4,     title="Consider flush if occurred in last N bars", minval=1, maxval=10)
showFlushMarkers    = input.bool(true, title="Show Flush Markers (debug)")

// === Force all calculations on 30m timeframe ===
tf    = "30"
is30m = timeframe.isintraday and timeframe.multiplier == 30

// === Get 30m OHLC & indicators ===
o30  = request.security(syminfo.tickerid, tf, open,  barmerge.gaps_off, barmerge.lookahead_off)
h30  = request.security(syminfo.tickerid, tf, high,  barmerge.gaps_off, barmerge.lookahead_off)
l30  = request.security(syminfo.tickerid, tf, low,   barmerge.gaps_off, barmerge.lookahead_off)
c30  = request.security(syminfo.tickerid, tf, close, barmerge.gaps_off, barmerge.lookahead_off)
atr30 = request.security(syminfo.tickerid, tf, ta.atr(atrLen), barmerge.gaps_off, barmerge.lookahead_off)

// avgBody30 = request.security(syminfo.tickerid, tf, ta.sma(math.abs(close - open), rangeMA_len), barmerge.gaps_off, barmerge.lookahead_off)

// === Candle colors (on 30m) ===
isGreen = c30 > o30
isRed   = c30 < o30

// === Flush detection (only red candles, body-based) ===
body30 = math.abs(c30 - o30)

// Condition: large red body compared to ATR or average body
// isFlushRed30 = useFlushDetect and isRed and body30 > math.max(atr30 * flushBodyMult, avgBody30 * bodyMultMA)
isFlushRed30 = useFlushDetect and isRed and body30 > (atr30 * flushBodyMult)
isFlushGreen30 = false  // not used in simplified version

// optional visual debug
if showFlushMarkers and isFlushRed30 and is30m
    label.new(bar_index, high + (high - low) * 0.2, text="Flush", style=label.style_label_down, color=color.red, textcolor=color.white, size=size.tiny, yloc=yloc.price)

// === Track streaks ===
var int greenStreak = 0
var int redStreak   = 0
greenStreak := isGreen ? greenStreak + 1 : 0
redStreak   := isRed   ? redStreak + 1   : 0

// === Candidate pivot storage ===
var int    candidateBar   = na
var float  candidateHigh  = na
var float  candidateLow   = na
var int    candidateDir   = 0   // -1 = bullish candidate, +1 = bearish candidate

// Helper: check if a flush occurred in last N bars
flushRedRecent = false
for i = 1 to flushLookback
    flushRedRecent := flushRedRecent or nz(isFlushRed30[i], false)

// --- Identify candidate pivots ---
// Only set candidate if none exists already
if bar_index > trendLength and na(candidateBar)
    // After red streak OR recent flush, a green candle → bullish candidate
    if (redStreak[1] >= trendLength or flushRedRecent) and isGreen
        candidateBar  := bar_index
        candidateHigh := h30
        candidateLow  := l30
        candidateDir  := -1

    // After green streak, a red candle → bearish candidate (unchanged)
    if greenStreak[1] >= trendLength and isRed
        candidateBar  := bar_index
        candidateHigh := h30
        candidateLow  := l30
        candidateDir  := 1


// --- Check confirmation ---
barsSinceCandidate = not na(candidateBar) ? bar_index - candidateBar : na
validCandidate     = not na(barsSinceCandidate) and barsSinceCandidate > 0 and barsSinceCandidate <= maxBarsAfterPivot

buyPivot  = validCandidate and candidateDir == -1 and h30 > candidateHigh
sellPivot = validCandidate and candidateDir ==  1 and l30 < candidateLow

// --- Expiration ---
expiredCandidate = not na(barsSinceCandidate) and barsSinceCandidate > maxBarsAfterPivot

// === Plot confirmed signals ===
if buyPivot and is30m
    label.new(bar_index, l30 - (h30 - l30) * 0.2, "P Buy",
              yloc=yloc.price, style=label.style_label_up,
              color=color.green, textcolor=color.white, size=size.small)
    if showPivotLines
        line.new(candidateBar, candidateHigh, bar_index, candidateHigh, color=color.green, width=1)
    // reset
    candidateBar  := na
    candidateHigh := na
    candidateLow  := na
    candidateDir  := 0

if sellPivot and is30m
    if showPivotLines
        line.new(candidateBar, candidateLow, bar_index, candidateLow, color=color.red, width=1)
    // reset
    candidateBar  := na
    candidateHigh := na
    candidateLow  := na
    candidateDir  := 0

if expiredCandidate
    candidateBar  := na
    candidateHigh := na
    candidateLow  := na
    candidateDir  := 0

// === Alerts ===
if enableBuyAlerts and is30m and buyPivot
    alert("Buy signal confirmed: price broke above pivot high", alert.freq_once_per_bar)

if enableSellAlerts and is30m and sellPivot
    alert("Sell signal confirmed: price broke below pivot low", alert.freq_once_per_bar)
